/*
 * Copyright 2014 Rivaldo.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jety.client;

import org.jety.client.transport.HttpClientTransport;
import org.jety.client.transport.AsynchronousTransport;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Random;
import org.jety.commons.exception.JsonRpcClientException;
import org.jety.commons.exception.JsonRpcRemoteException;
import org.jety.commons.util.AsynchronousUtil;
import org.jety.commons.util.CommunicationUtil;
import org.jety.commons.util.JsonConverter;
import org.jety.commons.util.JsonRpcConstants;
import org.jety.commons.util.JsonRpcErrorBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *
 * @author Rivaldo
 */
public class InvokeHandler implements InvocationHandler {

    private final String handlerName;
    private final HttpClientTransport transport;
    private final AsynchronousExecutor asyncExecutor;
    private static final Logger LOG = LoggerFactory.getLogger(InvokeHandler.class);

    public InvokeHandler(String handlerName, HttpClientTransport transport) {
        this.handlerName = handlerName;
        this.transport = transport;
        this.asyncExecutor = new AsynchronousExecutor();
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return invoke(method, args);
    }

    private Object invoke(Method method, Object[] args) {

        int requestID = getRequestID();

        String requestData = CommunicationUtil.creatMethodRequest(requestID, handlerName, method, args).toString();
        LOG.debug("JSON-RPC >>  {}", requestData);

        String responseData;

        if (AsynchronousUtil.isAsynchronous(method)) {
            AsynchronousTransport asyncTransport = new AsynchronousTransport(asyncExecutor, transport.getUrl());
            responseData = sendAsyncRequest(requestData, asyncTransport);
            asyncExecutor.addHandler("Async" + Integer.toString(requestID), method, args);
            asyncExecutor.addListner(asyncTransport);
        } else {
            responseData = sendSyncRequest(requestData);
        }

        LOG.debug("JSON-RPC <<  {}", responseData);

        JsonObject resp = JsonConverter.toJsonObject(responseData);
        checkErrorMessage(resp);

        if (method.getReturnType() == void.class) {
            return null;
        }

        JsonElement result = resp.get(JsonRpcConstants.PROPERTY_RESULT);
        return JsonConverter.fromJsonObject(result, method.getGenericReturnType());
    }

    private String sendSyncRequest(String requestData) throws JsonRpcClientException {
        String responseData = null;
        try {
            responseData = transport.call(requestData);
        } catch (Exception e) {
            LOG.error("Unable to get data from transport.", e);
            throw new JsonRpcClientException("Unable to get data from transport.", e);
        }
        return responseData;
    }

    private String sendAsyncRequest(String requestData, AsynchronousTransport asyncTransport) throws JsonRpcClientException {
        String responseData = null;
        try {
            responseData = asyncTransport.call(requestData);
        } catch (Exception e) {
            LOG.error("Unable to get data from transport.", e);
            throw new JsonRpcClientException("Unable to get data from transport.", e);
        }
        return responseData;
    }

    private void checkErrorMessage(JsonObject message) {

        JsonElement error = message.get(JsonRpcConstants.PROPERTY_ERROR);

        if (error != null && !error.isJsonNull()) {

            if (error.isJsonPrimitive()) {
                throw new JsonRpcRemoteException(error.getAsString());
            } else if (error.isJsonObject()) {
                throw getErrorFromObject(error.getAsJsonObject());
            } else {
                throw new JsonRpcRemoteException("Unknown error, data = " + error.toString());
            }
        }
    }

    private JsonRpcRemoteException getErrorFromObject(JsonObject error) {

        Integer code = null;
        String message = null;
        String data = null;

        if (error.has(JsonRpcConstants.PROPERTY_CODE)) {
            code = error.get(JsonRpcConstants.PROPERTY_CODE).getAsInt();
        }

        if (error.has(JsonRpcConstants.PROPERTY_MESSAGE)) {
            message = error.get(JsonRpcConstants.PROPERTY_MESSAGE).getAsString();
        }

        if (error.has(JsonRpcConstants.PROPERTY_DATA)) {

            JsonElement dataElement = error.get(JsonRpcConstants.PROPERTY_DATA);

            if (dataElement instanceof JsonObject) {
                data = error.get(JsonRpcConstants.PROPERTY_DATA).toString();
            } else {
                data = error.get(JsonRpcConstants.PROPERTY_DATA).getAsString();
            }
        }

        return new JsonRpcRemoteException(code, message, data);
    }

    private int getRequestID() {
        Random rand = new Random();
        return rand.nextInt(Integer.MAX_VALUE);
    }
}
